@towns-protocol/contracts 0.0.304 → 0.0.306
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/scripts/deployments/facets/DeployFeatureManager.s.sol +7 -6
- package/scripts/deployments/utils/DeployProxyBatchDelegation.s.sol +3 -0
- package/src/factory/facets/feature/FeatureManagerBase.sol +167 -0
- package/src/factory/facets/feature/FeatureManagerFacet.sol +20 -15
- package/src/factory/facets/feature/FeatureManagerStorage.sol +47 -0
- package/src/factory/facets/feature/IFeatureManagerFacet.sol +13 -7
- package/src/factory/facets/feature/FeatureConditionLib.sol +0 -53
- package/src/factory/facets/feature/FeatureManagerLib.sol +0 -168
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@towns-protocol/contracts",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.306",
|
|
4
4
|
"packageManager": "yarn@3.8.0",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "forge build",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@layerzerolabs/oapp-evm": "^0.3.2",
|
|
33
33
|
"@openzeppelin/merkle-tree": "^1.0.8",
|
|
34
34
|
"@prb/test": "^0.6.4",
|
|
35
|
-
"@towns-protocol/prettier-config": "^0.0.
|
|
35
|
+
"@towns-protocol/prettier-config": "^0.0.306",
|
|
36
36
|
"@wagmi/cli": "^2.2.0",
|
|
37
37
|
"account-abstraction": "https://github.com/eth-infinitism/account-abstraction/archive/refs/tags/v0.7.0.tar.gz",
|
|
38
38
|
"forge-std": "github:foundry-rs/forge-std#v1.10.0",
|
|
@@ -53,5 +53,5 @@
|
|
|
53
53
|
"publishConfig": {
|
|
54
54
|
"access": "public"
|
|
55
55
|
},
|
|
56
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "439633cf1571af6312b1cda88b578853be38afc4"
|
|
57
57
|
}
|
|
@@ -12,13 +12,14 @@ import {FeatureManagerFacet} from "src/factory/facets/feature/FeatureManagerFace
|
|
|
12
12
|
|
|
13
13
|
library DeployFeatureManager {
|
|
14
14
|
function selectors() internal pure returns (bytes4[] memory _selectors) {
|
|
15
|
-
_selectors = new bytes4[](
|
|
15
|
+
_selectors = new bytes4[](7);
|
|
16
16
|
_selectors[0] = FeatureManagerFacet.setFeatureCondition.selector;
|
|
17
|
-
_selectors[1] = FeatureManagerFacet.
|
|
18
|
-
_selectors[2] = FeatureManagerFacet.
|
|
19
|
-
_selectors[3] = FeatureManagerFacet.
|
|
20
|
-
_selectors[4] = FeatureManagerFacet.
|
|
21
|
-
_selectors[5] = FeatureManagerFacet.
|
|
17
|
+
_selectors[1] = FeatureManagerFacet.updateFeatureCondition.selector;
|
|
18
|
+
_selectors[2] = FeatureManagerFacet.getFeatureCondition.selector;
|
|
19
|
+
_selectors[3] = FeatureManagerFacet.getFeatureConditions.selector;
|
|
20
|
+
_selectors[4] = FeatureManagerFacet.getFeatureConditionsForSpace.selector;
|
|
21
|
+
_selectors[5] = FeatureManagerFacet.checkFeatureCondition.selector;
|
|
22
|
+
_selectors[6] = FeatureManagerFacet.disableFeatureCondition.selector;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
function makeCut(
|
|
@@ -15,6 +15,7 @@ import {DeployTownsMainnet} from "./DeployTownsMainnet.s.sol";
|
|
|
15
15
|
|
|
16
16
|
contract DeployProxyBatchDelegation is Deployer {
|
|
17
17
|
address internal constant CLAIMERS = 0x0bEe55b52d01C4D5d4D0cfcE1d6e0baE6722db05;
|
|
18
|
+
address internal constant CLAIMERS_SEPOLIA = 0xeeDCAB3c3B032D5627dcF5e1475E0e24a88b4A21;
|
|
18
19
|
address internal constant BASE_REGISTRY = 0x7c0422b31401C936172C897802CF0373B35B7698;
|
|
19
20
|
address internal constant BASE_REGISTRY_SEPOLIA = 0x08cC41b782F27d62995056a4EF2fCBAe0d3c266F;
|
|
20
21
|
address internal constant MESSENGER = 0x866E82a600A1414e583f7F13623F1aC5d58b0Afa;
|
|
@@ -77,6 +78,8 @@ contract DeployProxyBatchDelegation is Deployer {
|
|
|
77
78
|
function _getClaimers(address deployer) internal returns (address) {
|
|
78
79
|
if (block.chainid == 1) {
|
|
79
80
|
return CLAIMERS;
|
|
81
|
+
} else if (block.chainid == 11_155_111) {
|
|
82
|
+
return CLAIMERS_SEPOLIA;
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
vm.broadcast(deployer);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.23;
|
|
3
|
+
|
|
4
|
+
// interfaces
|
|
5
|
+
import {IFeatureManagerFacetBase} from "./IFeatureManagerFacet.sol";
|
|
6
|
+
import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol";
|
|
7
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
8
|
+
|
|
9
|
+
// libraries
|
|
10
|
+
import {FeatureManagerStorage} from "./FeatureManagerStorage.sol";
|
|
11
|
+
import {FeatureCondition} from "./IFeatureManagerFacet.sol";
|
|
12
|
+
import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
|
|
13
|
+
import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol";
|
|
14
|
+
import {LibCall} from "solady/utils/LibCall.sol";
|
|
15
|
+
|
|
16
|
+
// contracts
|
|
17
|
+
|
|
18
|
+
abstract contract FeatureManagerBase is IFeatureManagerFacetBase {
|
|
19
|
+
using EnumerableSetLib for EnumerableSetLib.Bytes32Set;
|
|
20
|
+
using CustomRevert for bytes4;
|
|
21
|
+
|
|
22
|
+
function _upsertFeatureCondition(
|
|
23
|
+
bytes32 featureId,
|
|
24
|
+
FeatureCondition calldata condition,
|
|
25
|
+
bool create
|
|
26
|
+
) internal {
|
|
27
|
+
_validateToken(condition);
|
|
28
|
+
|
|
29
|
+
FeatureManagerStorage.Layout storage $ = FeatureManagerStorage.getLayout();
|
|
30
|
+
if (create && !$.featureIds.add(featureId)) {
|
|
31
|
+
FeatureAlreadyExists.selector.revertWith();
|
|
32
|
+
} else if (!create && !$.featureIds.contains(featureId)) {
|
|
33
|
+
FeatureNotActive.selector.revertWith();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
$.conditions[featureId] = condition;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// @notice Retrieves the condition for a specific feature
|
|
40
|
+
/// @dev Returns the complete condition struct with all parameters
|
|
41
|
+
/// @param featureId The unique identifier for the feature
|
|
42
|
+
/// @return result The complete condition configuration for the feature
|
|
43
|
+
function _getFeatureCondition(
|
|
44
|
+
bytes32 featureId
|
|
45
|
+
) internal view returns (FeatureCondition memory result) {
|
|
46
|
+
FeatureManagerStorage.Layout storage $ = FeatureManagerStorage.getLayout();
|
|
47
|
+
assembly ("memory-safe") {
|
|
48
|
+
mstore(0x40, result)
|
|
49
|
+
}
|
|
50
|
+
result = $.conditions[featureId];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// @notice Retrieves all feature conditions
|
|
54
|
+
/// @dev Returns an array of all feature conditions
|
|
55
|
+
/// @return FeatureCondition[] An array of all feature conditions
|
|
56
|
+
function _getFeatureConditions() internal view returns (FeatureCondition[] memory) {
|
|
57
|
+
FeatureManagerStorage.Layout storage $ = FeatureManagerStorage.getLayout();
|
|
58
|
+
uint256 featureCount = $.featureIds.length();
|
|
59
|
+
|
|
60
|
+
FeatureCondition[] memory conditions = new FeatureCondition[](featureCount);
|
|
61
|
+
for (uint256 i; i < featureCount; ++i) {
|
|
62
|
+
conditions[i] = $.conditions[$.featureIds.at(i)];
|
|
63
|
+
}
|
|
64
|
+
return conditions;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// @notice Retrieves all feature conditions for a specific space
|
|
68
|
+
/// @dev Returns an array of all feature conditions that are active for the space
|
|
69
|
+
/// @return FeatureCondition[] An array of all feature conditions that are active for the space
|
|
70
|
+
function _getFeatureConditionsForSpace(
|
|
71
|
+
address space
|
|
72
|
+
) internal view returns (FeatureCondition[] memory) {
|
|
73
|
+
FeatureManagerStorage.Layout storage $ = FeatureManagerStorage.getLayout();
|
|
74
|
+
uint256 featureCount = $.featureIds.length();
|
|
75
|
+
|
|
76
|
+
FeatureCondition[] memory conditions = new FeatureCondition[](featureCount);
|
|
77
|
+
uint256 index;
|
|
78
|
+
|
|
79
|
+
for (uint256 i; i < featureCount; ++i) {
|
|
80
|
+
bytes32 id = $.featureIds.at(i);
|
|
81
|
+
FeatureCondition storage cond = $.conditions[id];
|
|
82
|
+
|
|
83
|
+
if (_isValidCondition(cond, space)) {
|
|
84
|
+
conditions[index++] = cond;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
assembly ("memory-safe") {
|
|
89
|
+
mstore(conditions, index)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return conditions;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// @notice Disables a feature by setting its active flag to false
|
|
96
|
+
/// @dev This does not delete the condition, only deactivates it
|
|
97
|
+
/// @param featureId The unique identifier for the feature to disable
|
|
98
|
+
function _disableFeatureCondition(bytes32 featureId) internal {
|
|
99
|
+
FeatureCondition storage condition = FeatureManagerStorage.getLayout().conditions[
|
|
100
|
+
featureId
|
|
101
|
+
];
|
|
102
|
+
if (!condition.active) FeatureNotActive.selector.revertWith();
|
|
103
|
+
condition.active = false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// @notice Sets a feature condition (wrapper for upsert with create=true)
|
|
107
|
+
/// @param featureId The unique identifier for the feature
|
|
108
|
+
/// @param condition The condition struct containing token, threshold, active status, and extra data
|
|
109
|
+
function _setFeatureCondition(bytes32 featureId, FeatureCondition calldata condition) internal {
|
|
110
|
+
_upsertFeatureCondition(featureId, condition, true);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// @notice Updates a feature condition (wrapper for upsert with create=false)
|
|
114
|
+
/// @param featureId The unique identifier for the feature
|
|
115
|
+
/// @param condition The condition struct containing token, threshold, active status, and extra data
|
|
116
|
+
function _updateFeatureCondition(
|
|
117
|
+
bytes32 featureId,
|
|
118
|
+
FeatureCondition calldata condition
|
|
119
|
+
) internal {
|
|
120
|
+
_upsertFeatureCondition(featureId, condition, false);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function _validateToken(FeatureCondition calldata condition) internal view {
|
|
124
|
+
if (condition.token == address(0)) InvalidToken.selector.revertWith();
|
|
125
|
+
|
|
126
|
+
// Check if the token implements IVotes.getVotes with proper return data
|
|
127
|
+
(bool success, bool exceededMaxCopy, bytes memory data) = LibCall.tryStaticCall(
|
|
128
|
+
condition.token,
|
|
129
|
+
gasleft(),
|
|
130
|
+
32,
|
|
131
|
+
abi.encodeCall(IVotes.getVotes, (address(this)))
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (!success || exceededMaxCopy || data.length != 32)
|
|
135
|
+
InvalidInterface.selector.revertWith();
|
|
136
|
+
|
|
137
|
+
// Check if the token implements ERC20.totalSupply with proper return data
|
|
138
|
+
(success, exceededMaxCopy, data) = LibCall.tryStaticCall(
|
|
139
|
+
condition.token,
|
|
140
|
+
gasleft(),
|
|
141
|
+
32,
|
|
142
|
+
abi.encodeCall(IERC20.totalSupply, ())
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (!success || exceededMaxCopy || data.length != 32)
|
|
146
|
+
InvalidInterface.selector.revertWith();
|
|
147
|
+
|
|
148
|
+
uint256 totalSupply = abi.decode(data, (uint256));
|
|
149
|
+
if (totalSupply == 0) InvalidTotalSupply.selector.revertWith();
|
|
150
|
+
if (condition.threshold > totalSupply) InvalidThreshold.selector.revertWith();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// @notice Checks if a condition should be included for a given space
|
|
154
|
+
/// @dev Returns true if the condition is active, has a valid token, and meets the threshold
|
|
155
|
+
/// @param condition The condition to check
|
|
156
|
+
/// @param space The space address to check against
|
|
157
|
+
/// @return True if the condition should be included, false otherwise
|
|
158
|
+
function _isValidCondition(
|
|
159
|
+
FeatureCondition memory condition,
|
|
160
|
+
address space
|
|
161
|
+
) internal view returns (bool) {
|
|
162
|
+
if (!condition.active) return false;
|
|
163
|
+
if (condition.token == address(0)) return false;
|
|
164
|
+
uint256 votes = IVotes(condition.token).getVotes(space);
|
|
165
|
+
return votes >= condition.threshold;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -3,10 +3,12 @@ pragma solidity ^0.8.23;
|
|
|
3
3
|
|
|
4
4
|
// interfaces
|
|
5
5
|
import {IFeatureManagerFacet} from "./IFeatureManagerFacet.sol";
|
|
6
|
+
import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol";
|
|
6
7
|
|
|
7
8
|
// libraries
|
|
8
|
-
import {
|
|
9
|
-
import {FeatureCondition
|
|
9
|
+
import {FeatureManagerBase} from "./FeatureManagerBase.sol";
|
|
10
|
+
import {FeatureCondition} from "./IFeatureManagerFacet.sol";
|
|
11
|
+
|
|
10
12
|
// contracts
|
|
11
13
|
import {OwnableBase} from "@towns-protocol/diamond/src/facets/ownable/OwnableBase.sol";
|
|
12
14
|
import {Facet} from "@towns-protocol/diamond/src/facets/Facet.sol";
|
|
@@ -14,10 +16,7 @@ import {Facet} from "@towns-protocol/diamond/src/facets/Facet.sol";
|
|
|
14
16
|
/// @title FeatureManagerFacet
|
|
15
17
|
/// @notice Manages feature conditions and checks for spaces
|
|
16
18
|
/// @dev This facet is responsible for managing feature conditions and checking if a space meets the condition for a feature to be enabled
|
|
17
|
-
contract FeatureManagerFacet is IFeatureManagerFacet, OwnableBase, Facet {
|
|
18
|
-
using FeatureManagerLib for FeatureManagerLib.Layout;
|
|
19
|
-
using FeatureConditionLib for FeatureCondition;
|
|
20
|
-
|
|
19
|
+
contract FeatureManagerFacet is IFeatureManagerFacet, OwnableBase, Facet, FeatureManagerBase {
|
|
21
20
|
function __FeatureManagerFacet_init() external onlyInitializing {
|
|
22
21
|
_addInterface(type(IFeatureManagerFacet).interfaceId);
|
|
23
22
|
}
|
|
@@ -27,7 +26,16 @@ contract FeatureManagerFacet is IFeatureManagerFacet, OwnableBase, Facet {
|
|
|
27
26
|
bytes32 featureId,
|
|
28
27
|
FeatureCondition calldata condition
|
|
29
28
|
) external onlyOwner {
|
|
30
|
-
|
|
29
|
+
_upsertFeatureCondition(featureId, condition, true);
|
|
30
|
+
emit FeatureConditionSet(featureId, condition);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// @inheritdoc IFeatureManagerFacet
|
|
34
|
+
function updateFeatureCondition(
|
|
35
|
+
bytes32 featureId,
|
|
36
|
+
FeatureCondition calldata condition
|
|
37
|
+
) external onlyOwner {
|
|
38
|
+
_upsertFeatureCondition(featureId, condition, false);
|
|
31
39
|
emit FeatureConditionSet(featureId, condition);
|
|
32
40
|
}
|
|
33
41
|
|
|
@@ -35,32 +43,29 @@ contract FeatureManagerFacet is IFeatureManagerFacet, OwnableBase, Facet {
|
|
|
35
43
|
function getFeatureCondition(
|
|
36
44
|
bytes32 featureId
|
|
37
45
|
) external view returns (FeatureCondition memory) {
|
|
38
|
-
return
|
|
46
|
+
return _getFeatureCondition(featureId);
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
/// @inheritdoc IFeatureManagerFacet
|
|
42
50
|
function getFeatureConditions() external view returns (FeatureCondition[] memory) {
|
|
43
|
-
return
|
|
51
|
+
return _getFeatureConditions();
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
/// @inheritdoc IFeatureManagerFacet
|
|
47
55
|
function getFeatureConditionsForSpace(
|
|
48
56
|
address space
|
|
49
57
|
) external view returns (FeatureCondition[] memory) {
|
|
50
|
-
return
|
|
58
|
+
return _getFeatureConditionsForSpace(space);
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
/// @inheritdoc IFeatureManagerFacet
|
|
54
62
|
function disableFeatureCondition(bytes32 featureId) external onlyOwner {
|
|
55
|
-
|
|
63
|
+
_disableFeatureCondition(featureId);
|
|
56
64
|
emit FeatureConditionDisabled(featureId);
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
/// @inheritdoc IFeatureManagerFacet
|
|
60
68
|
function checkFeatureCondition(bytes32 featureId, address space) external view returns (bool) {
|
|
61
|
-
|
|
62
|
-
if (!condition.isValid()) return false;
|
|
63
|
-
uint256 votes = condition.getVotes(space);
|
|
64
|
-
return condition.meetsThreshold(votes);
|
|
69
|
+
return _isValidCondition(_getFeatureCondition(featureId), space);
|
|
65
70
|
}
|
|
66
71
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.23;
|
|
3
|
+
|
|
4
|
+
// interfaces
|
|
5
|
+
|
|
6
|
+
// libraries
|
|
7
|
+
|
|
8
|
+
// contracts
|
|
9
|
+
import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol";
|
|
10
|
+
|
|
11
|
+
/// @notice Represents a condition for feature activation
|
|
12
|
+
/// @dev Used to determine if a feature should be enabled based on token voting power
|
|
13
|
+
/// @param token The address of the token used for voting (must implement IVotes)
|
|
14
|
+
/// @param threshold The minimum voting power (votes) required to activate the feature
|
|
15
|
+
/// @param active Whether the condition is currently active
|
|
16
|
+
/// @param extraData Additional data that might be used for specialized condition logic
|
|
17
|
+
struct FeatureCondition {
|
|
18
|
+
address token;
|
|
19
|
+
bool active;
|
|
20
|
+
uint256 threshold;
|
|
21
|
+
bytes extraData;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// @title FeatureManager
|
|
25
|
+
library FeatureManagerStorage {
|
|
26
|
+
using EnumerableSetLib for EnumerableSetLib.Bytes32Set;
|
|
27
|
+
|
|
28
|
+
// keccak256(abi.encode(uint256(keccak256("factory.facets.feature.manager.storage")) - 1)) & ~bytes32(uint256(0xff))
|
|
29
|
+
bytes32 constant DEFAULT_STORAGE_SLOT =
|
|
30
|
+
0x20c456a8ea15fcf7965033c954321ffd9dc82a2c65f686a77e2a67da65c29000;
|
|
31
|
+
|
|
32
|
+
/// @notice Storage layout for the FeatureManager
|
|
33
|
+
/// @dev Maps feature IDs to their activation conditions
|
|
34
|
+
/// @custom:storage-location erc7201:towns.storage.FeatureManager
|
|
35
|
+
struct Layout {
|
|
36
|
+
// Feature IDs
|
|
37
|
+
EnumerableSetLib.Bytes32Set featureIds;
|
|
38
|
+
// Feature ID => Condition
|
|
39
|
+
mapping(bytes32 featureId => FeatureCondition condition) conditions;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getLayout() internal pure returns (Layout storage $) {
|
|
43
|
+
assembly {
|
|
44
|
+
$.slot := DEFAULT_STORAGE_SLOT
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -2,11 +2,8 @@
|
|
|
2
2
|
pragma solidity ^0.8.23;
|
|
3
3
|
|
|
4
4
|
// interfaces
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import {FeatureCondition} from "./FeatureConditionLib.sol";
|
|
8
|
-
|
|
9
|
-
// contracts
|
|
5
|
+
import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol";
|
|
6
|
+
import {FeatureCondition} from "./FeatureManagerStorage.sol";
|
|
10
7
|
|
|
11
8
|
/// @title IFeatureManagerFacetBase
|
|
12
9
|
/// @notice Base interface for the FeatureManager facet defining errors and events
|
|
@@ -18,15 +15,18 @@ interface IFeatureManagerFacetBase {
|
|
|
18
15
|
/// @notice Error thrown when a token has zero total supply
|
|
19
16
|
error InvalidTotalSupply();
|
|
20
17
|
|
|
21
|
-
/// @notice Error thrown when an invalid token address is provided (zero address
|
|
18
|
+
/// @notice Error thrown when an invalid token address is provided (e.g., zero address)
|
|
22
19
|
error InvalidToken();
|
|
23
20
|
|
|
24
|
-
/// @notice Error thrown when
|
|
21
|
+
/// @notice Error thrown when the token does not implement the required interfaces (e.g., IVotes and ERC20 totalSupply)
|
|
25
22
|
error InvalidInterface();
|
|
26
23
|
|
|
27
24
|
/// @notice Error thrown when a feature condition is not active
|
|
28
25
|
error FeatureNotActive();
|
|
29
26
|
|
|
27
|
+
/// @notice Error thrown when a feature condition already exists
|
|
28
|
+
error FeatureAlreadyExists();
|
|
29
|
+
|
|
30
30
|
/// @notice Emitted when a feature condition is set or updated
|
|
31
31
|
/// @param featureId The unique identifier for the feature whose condition was set
|
|
32
32
|
/// @param condition The condition parameters that were set for the feature
|
|
@@ -44,6 +44,12 @@ interface IFeatureManagerFacet is IFeatureManagerFacetBase {
|
|
|
44
44
|
/// @dev Only callable by the contract owner
|
|
45
45
|
function setFeatureCondition(bytes32 featureId, FeatureCondition memory condition) external;
|
|
46
46
|
|
|
47
|
+
/// @notice Updates the condition for a feature
|
|
48
|
+
/// @param featureId The unique identifier for the feature
|
|
49
|
+
/// @param condition The condition struct containing token, threshold, active status, and extra data
|
|
50
|
+
/// @dev Only callable by the contract owner
|
|
51
|
+
function updateFeatureCondition(bytes32 featureId, FeatureCondition memory condition) external;
|
|
52
|
+
|
|
47
53
|
/// @notice Gets the condition for a feature
|
|
48
54
|
/// @param featureId The unique identifier for the feature
|
|
49
55
|
/// @return The condition struct for the specified feature
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity ^0.8.23;
|
|
3
|
-
|
|
4
|
-
// interfaces
|
|
5
|
-
import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol";
|
|
6
|
-
|
|
7
|
-
/// @notice Represents a condition for feature activation
|
|
8
|
-
/// @dev Used to determine if a feature should be enabled based on token voting power
|
|
9
|
-
/// @param token The address of the token used for voting (must implement IVotes)
|
|
10
|
-
/// @param threshold The minimum number of delegations required to activate the feature
|
|
11
|
-
/// @param active Whether the condition is currently active
|
|
12
|
-
/// @param extraData Additional data that might be used for specialized condition logic
|
|
13
|
-
struct FeatureCondition {
|
|
14
|
-
address token;
|
|
15
|
-
bool active;
|
|
16
|
-
uint256 threshold;
|
|
17
|
-
bytes extraData;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/// @title FeatureConditionLib
|
|
21
|
-
library FeatureConditionLib {
|
|
22
|
-
function isValid(FeatureCondition storage condition) internal view returns (bool) {
|
|
23
|
-
return condition.active && condition.token != address(0);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/// @notice Gets the voting power of a space for the condition's token
|
|
27
|
-
/// @dev Calls the getVotes function on the token contract
|
|
28
|
-
/// @param condition The condition containing the token address
|
|
29
|
-
/// @param space The address of the space to check votes for
|
|
30
|
-
/// @return The number of votes the space has with the token
|
|
31
|
-
function getVotes(
|
|
32
|
-
FeatureCondition storage condition,
|
|
33
|
-
address space
|
|
34
|
-
) internal view returns (uint256) {
|
|
35
|
-
return IVotes(condition.token).getVotes(space);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/// @notice Checks if the given number of votes meets the condition's threshold
|
|
39
|
-
/// @dev Returns false if condition is inactive, true if threshold is 0, otherwise compares
|
|
40
|
-
/// votes to threshold
|
|
41
|
-
/// @param condition The condition containing the threshold and active status
|
|
42
|
-
/// @param votes The number of votes to check against the threshold
|
|
43
|
-
/// @return True if the condition is met, false otherwise
|
|
44
|
-
function meetsThreshold(
|
|
45
|
-
FeatureCondition storage condition,
|
|
46
|
-
uint256 votes
|
|
47
|
-
) internal view returns (bool) {
|
|
48
|
-
if (!isValid(condition)) return false;
|
|
49
|
-
uint256 threshold = condition.threshold;
|
|
50
|
-
if (threshold == 0) return true;
|
|
51
|
-
return votes >= threshold;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity ^0.8.23;
|
|
3
|
-
|
|
4
|
-
// interfaces
|
|
5
|
-
|
|
6
|
-
import {IFeatureManagerFacetBase} from "./IFeatureManagerFacet.sol";
|
|
7
|
-
import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol";
|
|
8
|
-
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
9
|
-
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
10
|
-
|
|
11
|
-
// libraries
|
|
12
|
-
|
|
13
|
-
import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
|
|
14
|
-
import {FeatureCondition, FeatureConditionLib} from "./FeatureConditionLib.sol";
|
|
15
|
-
import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol";
|
|
16
|
-
|
|
17
|
-
/// @title FeatureManager
|
|
18
|
-
library FeatureManagerLib {
|
|
19
|
-
using EnumerableSetLib for EnumerableSetLib.Bytes32Set;
|
|
20
|
-
using FeatureConditionLib for FeatureCondition;
|
|
21
|
-
|
|
22
|
-
// keccak256(abi.encode(uint256(keccak256("factory.facets.feature.manager.storage")) - 1)) & ~bytes32(uint256(0xff))
|
|
23
|
-
bytes32 constant DEFAULT_STORAGE_SLOT =
|
|
24
|
-
0x20c456a8ea15fcf7965033c954321ffd9dc82a2c65f686a77e2a67da65c29000;
|
|
25
|
-
|
|
26
|
-
/// @notice Storage layout for the FeatureManager
|
|
27
|
-
/// @dev Maps feature IDs to their activation conditions
|
|
28
|
-
/// @custom:storage-location erc7201:towns.storage.FeatureManager
|
|
29
|
-
struct Layout {
|
|
30
|
-
// Feature IDs
|
|
31
|
-
EnumerableSetLib.Bytes32Set featureIds;
|
|
32
|
-
// Feature ID => Condition
|
|
33
|
-
mapping(bytes32 featureId => FeatureCondition condition) conditions;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/// @notice Sets the condition for a specific feature
|
|
37
|
-
/// @dev Validates token, threshold, and total supply to ensure proper configuration
|
|
38
|
-
/// @param self The storage layout pointer
|
|
39
|
-
/// @param featureId The unique identifier for the feature
|
|
40
|
-
/// @param condition The condition parameters for feature activation
|
|
41
|
-
/// @custom:error InvalidToken Thrown when token is address(0) or doesn't support IVotes
|
|
42
|
-
/// @custom:error InvalidTotalSupply Thrown when token total supply is 0
|
|
43
|
-
/// @custom:error InvalidThreshold Thrown when threshold exceeds total supply
|
|
44
|
-
function setFeatureCondition(
|
|
45
|
-
Layout storage self,
|
|
46
|
-
bytes32 featureId,
|
|
47
|
-
FeatureCondition calldata condition
|
|
48
|
-
) internal {
|
|
49
|
-
if (condition.token == address(0)) {
|
|
50
|
-
CustomRevert.revertWith(IFeatureManagerFacetBase.InvalidToken.selector);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
_validateGetVotes(condition);
|
|
54
|
-
|
|
55
|
-
// Check totalSupply directly
|
|
56
|
-
try IERC20(condition.token).totalSupply() returns (uint256 totalSupply) {
|
|
57
|
-
if (totalSupply == 0) {
|
|
58
|
-
CustomRevert.revertWith(IFeatureManagerFacetBase.InvalidTotalSupply.selector);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (condition.threshold > totalSupply) {
|
|
62
|
-
CustomRevert.revertWith(IFeatureManagerFacetBase.InvalidThreshold.selector);
|
|
63
|
-
}
|
|
64
|
-
} catch {
|
|
65
|
-
CustomRevert.revertWith(IFeatureManagerFacetBase.InvalidToken.selector);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
self.featureIds.add(featureId);
|
|
69
|
-
self.conditions[featureId] = condition;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/// @notice Retrieves the condition for a specific feature
|
|
73
|
-
/// @dev Returns the complete condition struct with all parameters
|
|
74
|
-
/// @param self The storage layout pointer
|
|
75
|
-
/// @param featureId The unique identifier for the feature
|
|
76
|
-
/// @return Condition memory The complete condition configuration for the feature
|
|
77
|
-
function getFeatureCondition(
|
|
78
|
-
Layout storage self,
|
|
79
|
-
bytes32 featureId
|
|
80
|
-
) internal view returns (FeatureCondition memory) {
|
|
81
|
-
return self.conditions[featureId];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/// @notice Retrieves all feature conditions
|
|
85
|
-
/// @dev Returns an array of all feature conditions
|
|
86
|
-
/// @param self The storage layout pointer
|
|
87
|
-
/// @return Condition[] An array of all feature conditions
|
|
88
|
-
function getFeatureConditions(
|
|
89
|
-
Layout storage self
|
|
90
|
-
) internal view returns (FeatureCondition[] memory) {
|
|
91
|
-
uint256 featureCount = self.featureIds.length();
|
|
92
|
-
|
|
93
|
-
FeatureCondition[] memory conditions = new FeatureCondition[](featureCount);
|
|
94
|
-
for (uint256 i; i < featureCount; ++i) {
|
|
95
|
-
conditions[i] = self.conditions[self.featureIds.at(i)];
|
|
96
|
-
}
|
|
97
|
-
return conditions;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/// @notice Retrieves all feature conditions for a specific space
|
|
101
|
-
/// @dev Returns an array of all feature conditions that are active for the space
|
|
102
|
-
/// @param self The storage layout pointer
|
|
103
|
-
/// @param space The address of the space to check conditions for
|
|
104
|
-
/// @return Condition[] An array of all feature conditions that are active for the space
|
|
105
|
-
function getFeatureConditionsForSpace(
|
|
106
|
-
Layout storage self,
|
|
107
|
-
address space
|
|
108
|
-
) internal view returns (FeatureCondition[] memory) {
|
|
109
|
-
uint256 featureCount = self.featureIds.length();
|
|
110
|
-
|
|
111
|
-
FeatureCondition[] memory conditions = new FeatureCondition[](featureCount);
|
|
112
|
-
uint256 index;
|
|
113
|
-
|
|
114
|
-
for (uint256 i; i < featureCount; ++i) {
|
|
115
|
-
FeatureCondition storage condition = self.conditions[self.featureIds.at(i)];
|
|
116
|
-
uint256 votes = condition.getVotes(space);
|
|
117
|
-
|
|
118
|
-
if (condition.meetsThreshold(votes)) {
|
|
119
|
-
conditions[index++] = condition;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
assembly ("memory-safe") {
|
|
124
|
-
mstore(conditions, index)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return conditions;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/// @notice Disables a feature by setting its active flag to false
|
|
131
|
-
/// @dev This does not delete the condition, only deactivates it
|
|
132
|
-
/// @param self The storage layout pointer
|
|
133
|
-
/// @param featureId The unique identifier for the feature to disable
|
|
134
|
-
function disableFeatureCondition(Layout storage self, bytes32 featureId) internal {
|
|
135
|
-
FeatureCondition storage condition = self.conditions[featureId];
|
|
136
|
-
if (!condition.active) {
|
|
137
|
-
CustomRevert.revertWith(IFeatureManagerFacetBase.FeatureNotActive.selector);
|
|
138
|
-
}
|
|
139
|
-
condition.active = false;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function _validateGetVotes(FeatureCondition calldata condition) internal view {
|
|
143
|
-
// Check IVotes support by attempting to call getVotes
|
|
144
|
-
try IVotes(condition.token).getVotes(address(this)) returns (uint256) {
|
|
145
|
-
// If we get here, getVotes is supported
|
|
146
|
-
return;
|
|
147
|
-
} catch {
|
|
148
|
-
CustomRevert.revertWith(IFeatureManagerFacetBase.InvalidInterface.selector);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/// @notice Retrieves the layout for the FeatureManager at the default storage slot
|
|
153
|
-
/// @dev This is accessed by the facet contract
|
|
154
|
-
/// @return $ The layout storage pointer
|
|
155
|
-
function getLayout() internal pure returns (Layout storage $) {
|
|
156
|
-
return getLayout(DEFAULT_STORAGE_SLOT);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/// @notice Retrieves the layout for the FeatureManager at a custom storage slot
|
|
160
|
-
/// @dev This function is used to access the storage layout for a given slot
|
|
161
|
-
/// @param slot The storage slot to retrieve the layout for
|
|
162
|
-
/// @return $ The layout storage pointer
|
|
163
|
-
function getLayout(bytes32 slot) internal pure returns (Layout storage $) {
|
|
164
|
-
assembly {
|
|
165
|
-
$.slot := slot
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|